package edu.pitt.cs.cs2510.project3.model;

/*
 * File: FrontEnd.java
 * Authors: Jesse Szwedko, Callen Shaw, Heather Friedberg
 * CS2510 Project 3
 */

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;

import edu.pitt.cs.cs2510.project3.algorithm.*;
import edu.pitt.cs.cs2510.project3.configuration.InputParameters;

public abstract class FrontEnd extends Server {

	public final float MAX_WORKER_LOAD;
	final float REPLICATION_HOTNESS;
	
	protected List<WorkerServer> workers;
	public Map<Integer, FrontEnd> frontEnds;

	protected Queue<Message> messages;
	protected Queue<Message> completedMessages;

	private double cost = 0;
	
	protected final LoadBalancer loadBal;
	protected final Deleter delete;

	public FrontEnd(int id, InputParameters simulationParameters, Object m) {
		super(id, simulationParameters, m);
		MAX_WORKER_LOAD = (float) simulationParameters.getOverLoadThreshold();
		for (int i = 0; i < simulationParameters.getFilesAtFrontend(id).length; i++)
		{
			this.files.put(simulationParameters.getFilesAtFrontend(id)[i], new File(simulationParameters.getFilesAtFrontend(id)[i], simulationParameters.getBlocksPerFile(), 0, true));
		}
		REPLICATION_HOTNESS = (float)simulationParameters.getReplicateHotness();
		this.workers = new CopyOnWriteArrayList<WorkerServer>();
		this.messages = new ConcurrentLinkedQueue<Message>();
		this.completedMessages = new LinkedList<Message>();
		
		this.frontEnds = new HashMap<Integer, FrontEnd>();

		loadBal = new LoadBalancer(this.simulationParameters, this);
		delete = new Deleter(simulationParameters, this);
	}

	@Override
	public void run() {		
		while (true && !this.die)
		{			
			while (this.messages.peek() != null) //process any waiting messages
			{
				Message message = this.messages.poll();
				this.handleMessage(message);
				this.completedMessages.add(message);
				System.out.println("MSG: " + message.toString());
			}

			while (this.requests.peek() != null)
			{
				Request request = this.requests.poll();
				this.handleRequest(request);
				this.completedRequests.add(request);
				System.out.println("FE-REQ: " + request.toString());
			}


			//we aren't ready for the next clock cycle yet so sleep a bit and try again
			if (!this.tick)
			{		
				try {
					synchronized(this.monitor) {
						if (!this.messages.isEmpty() || !this.requests.isEmpty())
						{
							continue;
						}
						this.monitor.wait();
					}
				} catch (InterruptedException e1) {
					e1.printStackTrace();
				}
				continue;
			}

			//let workers service requests
			for (WorkerServer worker : this.workers)
			{
				worker.time = this.time;
				worker.tick = true;
			}
			
			synchronized (this)
			{
				this.notifyAll();
			}

			boolean workersDone;
			do {
				workersDone = true;
				for (WorkerServer worker : this.workers)
				{
					if (worker.tick)
					{
						workersDone = false;
						try {
							Thread.sleep(1);
						} catch (InterruptedException e) {
							System.err.println("Frontend " + this.toString() + " interrupted while waiting on workers");
							e.printStackTrace();
						}

						break;
					}
				}
			}while (!workersDone);

			//run respective load balancing algorithms
			this.runAlgorithm();

			//TODO decrease hotness of files

			this.cost += this.uptimeCost();

			this.tick = false;
		}
	}
	
	@Override
	public boolean isReady() {
		
		for (Server workers : this.workers)
		{
			if (!workers.isReady())
			{
				return false;
			}
		}
		
		return this.requests.isEmpty() && this.messages.isEmpty() && !this.tick;
	}
	
	@Override
	public boolean isDone() {
		for (Server worker : this.workers)
		{
			if (!worker.isDone())
			{
				return false;
			}
		}
		return this.requests.isEmpty() && this.messages.isEmpty() && !this.tick;
	}
	
	public void addRequest(Request request)
	{
		request.frontEnd = this;
		super.addRequest(request);
	}

	private double uptimeCost()
	{
		//TODO
		return 0.0;
	}

	/**
	 * Processes a message from another frontend
	 * @param message The message to process
	 */
	protected void handleMessage(Message message) {
		switch (message.type){
		case REQUEST_REPLICATION:
			boolean wantsReplica = false;
			WorkerServer temp = null;
			if (!hasFile(message.file.fileId)){
				float load = Float.MAX_VALUE;
				int space = 0;
				for (WorkerServer w : workers){
					if(w.files.isEmpty()){
						temp = w;
						break;
					}
					int remainingSpace = w.remainingSpace();
					if (remainingSpace >= (message.file.size) && w.getLoad() < MAX_WORKER_LOAD && w.getLoad() <= load && remainingSpace > space){
						temp = w;
						load = w.getLoad();
						space = remainingSpace;
					}
				}
				if (temp != null){
					wantsReplica = true;
				}
			}
			Message m = new Message(Message.Type.REQUEST_REPLICATION_ACK, this, message.originServer);
			m.replicateFrom = message.replicateFrom;
			if (temp != null){
				m.replicateTo = temp.getUniqueIdentifier();
			}
			m.file = message.file;
			m.answer = wantsReplica;
			sendMessage(m);
			break;
		case REQUEST_REPLICATION_ACK:
			if(message.originServer.getWorkerServer(message.replicateTo) == null){
				break;
			}
			if (message.answer){
				System.out.println("REPLICATE: (between frontends) server " + this.id + " moved file {" + message.file.fileId + "} from worker {" + message.replicateFrom + "} to {" + message.replicateTo + "}");
				if((getWorkerServer(message.replicateFrom)).sendReplication(message.file, message.originServer.getWorkerServer(message.replicateTo))){
					if(!message.originServer.files.containsKey(message.file.fileId)){
						File newFile = message.file.clone();
						newFile.workerServer = getWorkerServer(message.replicateFrom);
						message.originServer.files.put(message.file.fileId, newFile);
					}
				}
			}
			break;
		case REQUEST_LOAD:
			this.sendMessage(this.createLoadMsg(message.originServer));
			break;
		case RECEIVED_REQUEST:
			handleRequest(message.requests.poll());
			if(message.requests.size() > 0)
				System.out.println("Must change handleMessage to accept multiple requests in queue!!!");
			break;
		case REQUEST_LOAD_ACK:
			loadBal.offloadServerResponded(message);
			break;
		default:
			break;
		}
	}
	
	/**
	 * Handles an incoming request and routes it to a worker
	 * @param request The request to handle
	 */
	protected void handleRequest(Request request){
		files.get(request.fileId).touch(this.time);
		
		WorkerServer temp = null;
		float load = Float.MAX_VALUE;
		
		for(WorkerServer w : workers){
			if(w.hasFileBlock(request.fileId, request.currentBlockId) && w.getLoad() < load){
				temp = w;
				load = w.getLoad();
			}
		}
		
		if(temp != null){
			temp.addRequest(request);
			if (request.currentBlockId == 0){
				handleReplication(request, temp.getUniqueIdentifier());
			}
		}
	}
	
	protected void handleReplication(Request r, String source){
		WorkerServer target = getReplicationTarget(r, source);
		if (target != null){
			System.out.println("REPLICATE: (within server) server:" + this.id + " moved file {" + r.fileId + "} from worker + {" + source + "} to {" + target.getUniqueIdentifier() + "}");
			getWorkerServer(source).sendReplication(files.get(r.fileId), target);	
		}
		
		File requestedFile = files.get(r.fileId);
		System.out.println("Current Time:" + this.time + " Interval:" + simulationParameters.getHotnessInterval());
		System.out.println("HOTNESS: " + requestedFile.getHotness(this.time, simulationParameters.getHotnessInterval()));
		if (requestedFile.getHotness(this.time, simulationParameters.getHotnessInterval()) > REPLICATION_HOTNESS){
			for (FrontEnd f : frontEnds.values()){
				Message replMsg = new Message(Message.Type.REQUEST_REPLICATION, this, f);
				replMsg.file = files.get(r.fileId);
				replMsg.replicateFrom = source;
				sendMessage(replMsg);
			}
		}
	}

	abstract protected WorkerServer getReplicationTarget(Request request, String source);
	
	private synchronized void addMessage(Message message) {
		if (message == null)
			try {
				throw new Exception();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		this.messages.add(message);
		this.wakeUp();
	}

	
	/**
	 * Sends a message to the message.destinationServer
	 * @param message The message to be sent
	 */
	public void sendMessage(Message message) {
		message.destinationServer.addMessage(message);
	}

	
	/**
	 * Runs the load balancing algorithm
	 */
	protected void runAlgorithm() {
		delete.runAlgorithm();
		loadBal.runAlgorithm();
	}
	
	@Override
	public void noMoreRequests(boolean noMoreRequests) {
		this.noMoreRequests = noMoreRequests;
		
		for (WorkerServer workerServer : this.workers)
		{
			workerServer.noMoreRequests(noMoreRequests);
		}	
	}
	
	public WorkerServer getWorker(int index) {
		return workers.get(index);
	}
	public int getWorkerCount() {
		return workers.size();
	}


	public void die()
	{
		for (WorkerServer workerServer : this.workers)
		{
			workerServer.die();
		}
		this.die = true;
	}
	
	@Override
	public String getUniqueIdentifier() {
		return Integer.toString(this.id);
	}
	
	public WorkerServer getWorkerServer(String uniqueIdentifier){
		for(WorkerServer w: workers){
			if(w.getUniqueIdentifier().equals(uniqueIdentifier)){
				return w;
			}
		}
		return null;
	}
	
	public synchronized void wakeUp(){
		synchronized(this.monitor){
			this.monitor.notifyAll();
		}
	}
	
	@Override
	public boolean hasFileBlock(int fileId, int blockId) {
		
		for (WorkerServer workerServer : this.workers)
		{
			if (workerServer.hasFileBlock(fileId, blockId))
			{
				return true;
			}
		}
		
		return false;
	}
	
	public void printWorkers(){
		for(WorkerServer w : workers){
			System.out.println(w);
		}
	}
	Message createLoadMsg(FrontEnd inquirer) {
		Message m = new Message(Message.Type.REQUEST_LOAD_ACK, this, inquirer);
		return m;
	}
	
	@Override
	protected double getUptimeCost() {
		double uptimeCost = 0;
		
		for (WorkerServer worker : this.workers)
		{
			uptimeCost += worker.getUptimeCost();
		}
		
		uptimeCost += this.simulationParameters.getUptimeCost();
		
		return uptimeCost;
	}
}
